package net.homeip.jcperera.nu.core.config;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.util.Properties;

import javax.ejb.EJB;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;

import net.homeip.jcperera.nu.core.CoreBase;
import net.homeip.jcperera.nu.core.msg.Logger;
import net.homeip.jcperera.nu.core.utils.ResourceUtil;

/**
 * <p>
 * Implementation of Stateful Session Bean Configuration.
 * </p>
 * <p>
 * Stores the {@code System Configuration} into memory and database tables, capable of using the
 * {@code Oracle Coherence Cache} if available (configurable using a property named
 * {@code nu.core.config.use.coherence.cache}) in addition to the database (to actually make faster the DB properties
 * support).
 * </p>
 * <p>
 * All properties stored in the configuration must be of type {@link String} and having a property name using standard
 * java naming convention for properties. Please do not use any property name starting with {@code nu.} as these are
 * reserved by the system.
 * </p>
 * <p>
 * You can always put properties into the DB and Coherence Cache (if applicable) by using the {@link put(String,String)}
 * method. Likewise, to read values of properties use the {@link get(String)} or the {@link get(String,String)} method.
 * </p>
 * <p>
 * There is a precedence order for properties both in DB and configuration file. Configuration file always takes
 * precedence over DB.
 * </p>
 * 
 * @name Global Configuration Bean
 * @author julio.c.perera@gmail.com
 * @application nu
 * @since 0.1.0.0
 */

@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
@LocalBean
@ApplicationScoped
@Named(Configuration.componentName)
public class Configuration extends CoreBase implements Serializable
{
    // Required to implement Serializable interface
    private static final long     serialVersionUID       = -4685752195983942019L;

    // CoreBase properties, for easy use from other components, as well as logging
    public static final int       runtimeLevel           = 0;
    public static final String    moduleName             = "core";
    public static final String    componentName          = "nu_core_configuration_bean";

    // Location of relevant configuration file
    private URL                   configFileLocation     = null;

    // Configuration file path and name for properties file containing
    // configuration properties
    public static final String    CONFIG_FILE_PATH       = "resources/config";
    public static final String    CONFIG_FILE_NAME       = "Configuration.properties";
    public static final String    CONFIG_FILE_PATHNAME   = CONFIG_FILE_PATH + "/" + CONFIG_FILE_NAME;
    public static final String    coherenceCachePropName = "nu.core.config.use.coherence.cache";

    // Get the log
    protected static final Logger log                    = Logger.getLogger();

    private String                nudbclass;
    private String                nudburl;
    private String                nudbuser;
    private String                nudbpass;

    private final Properties      properties             = new Properties();

    @EJB
    private CoherenceCache        coherenceCache;

    private static boolean        enableCoherenceCache   = false;

    public Configuration()
    {
    }

    /**
     * <p>
     * Initializes this {@link Configuration} instance
     * </p>
     * <p>
     * Reads the properties from the <code>Configuration.properties</code> files, injecting these key-value pairs in the
     * current object.
     * </p>
     */
    public void initialize(String nudbclass, String nudburl, String nudbuser, String nudbpass) throws Exception
    {
        log.info("Initializing Configuration...");
        this.nudbclass = nudbclass;
        this.nudburl = nudburl;
        this.nudbuser = nudbuser;
        this.nudbpass = nudbpass;

        try
        {
            int prevSize = this.properties.size();
            load();
            if (prevSize == 0) log.info("Read {0} properties from {1} file",
                    new String[] { String.valueOf(this.properties.size()), CONFIG_FILE_NAME });
            else log.info("Found already loaded {0} properties", new String[] { String.valueOf(prevSize) });

            if (get("nu.core.config.use.coherence.cache") != null
                    && get(coherenceCachePropName).trim().equalsIgnoreCase("true")) enableCoherenceCache = true;

            if (enableCoherenceCache) coherenceCache.initialize();
        }
        catch (Exception exception)
        {
            log.error("Error initializing configuration, cannot continue: '" + exception.getLocalizedMessage() + "'",
                    exception);
        }
    }

    /**
     * <p>
     * Start this {@link Configuration} instance
     * </p>
     * <p>
     * Retrieves all configuration values from the database and re-loads the configuration files effectively replacing
     * these values from the values from the <code>Configuration.properties</code> files
     * </p>
     */
    public void start() throws Exception
    {
        log.info("Starting Configuration...");
        try
        {
            // persistence = Boot.getSystem().getPersistence();
            // EntityManagerFactory emf = persistence.getEntityManagerFactory();
            // EntityManager em = (EntityManager) emf.createEntityManager();

            // TODO: Read from database
        }
        catch (Exception exception)
        {
            log.error("Error reading configuration, cannot continue: '" + exception.getLocalizedMessage() + "'",
                    exception);
        }
    }

    /**
     * Initialize local DB properties as loaded from init servlet, to be probably loaded from the
     * {@code Configuration.properties} file
     * 
     * @param nudbclass
     *            - JDBC class driver to use
     * @param nudburl
     *            - JDBC URL to use
     * @param nudbuser
     *            - Database User
     * @param nudbpass
     *            - Database Password (for User)
     */
    private void configureDBProperties()
    {
        this.properties.put("nu.db.class", nudbclass);
        this.properties.put("nu.db.url", nudburl);
        this.properties.put("nu.db.user", nudbuser);
        this.properties.put("nu.db.password", nudbpass);
    }

    /**
     * Gets the location of the properties configuration files
     */
    private void loadConfigFileLocation() throws IOException
    {
        if (configFileLocation == null)
        {
            URL url = ResourceUtil.getURLfromResourceName(CONFIG_FILE_PATHNAME);
            if (url != null) configFileLocation = url;
        }
    }

    /**
     * <p>
     * Loads the data associated to configuration variables.
     * </p>
     * <p>
     * Only reloads the files is there is no configuration loaded yet
     * </p>
     */
    private void load() throws Exception
    {
        configureDBProperties();
        loadConfigFileLocation();
        if (this.properties.size() == 4) this.properties.putAll(loadConfFile(configFileLocation));
    }

    /**
     * Reads configuration variables from file given the URL location.
     * 
     * @return The <code>Properties</code> object (as <code>HashMap</code>) containing the key=value <code>Map</code>.
     * @param url
     *            The location of the file, as a resource URL.
     */
    private Properties loadConfFile(URL url) throws Exception
    {
        Properties props = new Properties();
        try
        {
            log.info("Reading properties file from URL = {0}", new String[] { url.toExternalForm() });
            props.load(url.openStream());
        }
        catch (FileNotFoundException exception)
        {
            log.error("Properties file NOT FOUND, at URL = {0}", new String[] { url.toExternalForm() });
            throw exception;
        }
        catch (IOException exception)
        {
            log.error("IOError when reading Properties file, at URL = {0} ", new String[] { url.toExternalForm() });
            throw exception;
        }
        ;
        return props;
    }

    /**
     * <p>
     * Reloads the data associated to <code>Configuration.properties</code> files and then the database,
     * unconditionally.
     * </p>
     */
    public void reload()
    {
        synchronized (this)
        {
            try
            {
                log.info("Reloading configuration at request...");
                this.properties.clear();
                load();
                if (get(coherenceCachePropName) != null && get(coherenceCachePropName).trim().equalsIgnoreCase("true"))
                    enableCoherenceCache = true;
            }
            catch (Exception exception)
            {
                log.error("Error while reloading configuration, cannot continue: '" + exception.getLocalizedMessage()
                        + "'", exception);
            }
        }
    }

    /**
     * @see Properties#getProperty(String key)
     */
    public String get(String key)
    {
        String result = null;
        synchronized (this)
        {
            if (properties.containsKey(key)) result = properties.getProperty(key);
            else if (enableCoherenceCache) result = coherenceCache.get(key);
        }
        return result;
    }

    /**
     * @see Properties#containsKey(String key)
     */
    public boolean containsKey(String key)
    {
        boolean result;
        if (enableCoherenceCache) result = coherenceCache.containsKey(key);
        else result = properties.containsKey(key);
        return result;
    }

    /**
     * @see Properties#getProperty(String key, String defaultValue)
     */
    public String get(String key, String defaultValue)
    {
        String result = null;
        synchronized (this)
        {
            if (properties.containsKey(key)) result = properties.getProperty(key, defaultValue);
            else if (enableCoherenceCache) result = coherenceCache.get(key);
        }
        if (result == null) result = defaultValue;
        return result;
    }

    /**
     * <p>
     * Gets the current value for the <code>Configuration</code> property.
     * </p>
     * <p>
     * If the property is not in the <code>Configuration</code> sets the property to the specified
     * <code>defaultValue</code> and returns the <code>defaultValue</code>
     * </p>
     * 
     * @param key
     *            The property name (key)
     * @param defaultValue
     *            The <code>defaultValue</code> to set the property value to in case it does not exist
     * @return The value of the property or the default value, after calling this method the property always exists in
     *         the configuration
     */
    public String getAndPutIfNotExists(String key, String defaultValue)
    {
        String result = get(key);
        if (result == null)
        {
            result = defaultValue;
            put(key, defaultValue);
        }
        return result;
    }

    /**
     * <p>
     * Sets the <code>Configuration</code> property to the specified value, if no such property exists with the provided
     * key, a new property is created and added to the configuration
     * </p>
     * <p>
     * Returns the previous value of the <code>Configuration</code> property or <code>null</code> if no such property
     * existed previously
     * </p>
     * 
     * @see Properties#setProperty(String, String)
     */
    public String put(String key, String value)
    {
        synchronized (this)
        {
            if (enableCoherenceCache) return (String) coherenceCache.put(key, value);
            else return (String) this.properties.setProperty(key, value);
        }
    }

    // Destroy
    public void stop()
    {
        synchronized (this)
        {
            this.properties.clear();
        }
        log.info("Stopping Configuration...");
    }

    public CoherenceCache getCoherenceCache()
    {
        return coherenceCache;
    }

    public static boolean isEnableCoherenceCache()
    {
        return enableCoherenceCache;
    }

}
